package com.baomidou.mybatisplus.spring.mapper;

import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.mapper.ClassPathMapperScanner;
import org.mybatis.spring.mapper.MapperFactoryBean;
import org.springframework.asm.ClassWriter;
import org.springframework.asm.Opcodes;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.annotation.AnnotatedGenericBeanDefinition;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.context.annotation.AnnotationBeanNameGenerator;
import org.springframework.context.annotation.AnnotationConfigUtils;
import org.springframework.context.annotation.ScopeMetadata;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.util.StringUtils;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.spring.annotation.MapperEntity;

import lombok.Setter;
import lombok.extern.slf4j.Slf4j;

/**
 * Scan Entitys and Generator their Mapper interface
 * Reference {@link ClassPathMapperScanner} to scan Entity and Use ASM API to generator Mapper
 * @author Yangtze
 */
@Slf4j
public class MapperEntityScanner extends ClassPathMapperScanner implements BeanClassLoaderAware {
	private static ScopeMetadata DEFAULT_SCOPE = new ScopeMetadata();
	static {
		DEFAULT_SCOPE.setScopedProxyMode(ScopedProxyMode.NO);
		DEFAULT_SCOPE.setScopeName("singleton");
	}
	
	private MapperClassLoader loader;
	private BeanNameGenerator beanNameGenerator = new AnnotationBeanNameGenerator();
	private BeanDefinitionRegistry registry;
	
	@Setter
	private SqlSessionFactory sqlSessionFactory;
	@Setter
	private SqlSessionTemplate sqlSessionTemplate;
	@Setter
	private String sqlSessionTemplateBeanName;
	@Setter
	private String sqlSessionFactoryBeanName;
	@Setter
	private Class<? extends MapperFactoryBean> mapperFactoryBeanClass = MapperFactoryBean.class;
	
	public MapperEntityScanner(BeanDefinitionRegistry registry) {
        super(registry);
        this.registry = registry;
    }
	
	public void registerFilters() {
		addIncludeFilter(new AnnotationTypeFilter(MapperEntity.class));
	};
	
	@Override
	protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
		return true;
	}

    @Override
	public Set<BeanDefinitionHolder> doScan(String... basePackages) {
		Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
		
		for (String basePackage : basePackages) {
			Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
			for (BeanDefinition candidate : candidates) {
				
				BeanDefinitionHolder holder = createMapperBean(candidate.getBeanClassName());
				
				if(holder != null){
					beanDefinitions.add(holder);
				}
			}

		}
		
		beanDefinitions.forEach(b -> log.info(b.toString()));
		return beanDefinitions;
	}

	private BeanDefinitionHolder createMapperBean(String entityClassName) {
		Class mapperClass = loader.getMapperClass(entityClassName);

		String beanClassName = mapperClass.getName();

		GenericBeanDefinition definition = new AnnotatedGenericBeanDefinition(mapperClass);
		String beanName = this.beanNameGenerator.generateBeanName(definition, this.registry);
		
		if (definition instanceof AbstractBeanDefinition) {
			postProcessBeanDefinition((AbstractBeanDefinition) definition, beanName);
		}
		if (definition instanceof AnnotatedBeanDefinition) {
			AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) definition);
		}
		
		definition.getConstructorArgumentValues().addGenericArgumentValue(mapperClass);

		definition.setBeanClass(this.mapperFactoryBeanClass);
	    definition.getPropertyValues().add("addToConfig", true);
		
		log.debug("Creating MapperFactoryBean with name '{}' and '{}' mapperInterface", beanName, beanClassName);

		boolean explicitFactoryUsed = false;
		if (StringUtils.hasText(this.sqlSessionFactoryBeanName)) {
			definition.getPropertyValues().add("sqlSessionFactory",
					new RuntimeBeanReference(this.sqlSessionFactoryBeanName));
			explicitFactoryUsed = true;
		} else if (this.sqlSessionFactory != null) {
			definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory);
			explicitFactoryUsed = true;
		}

		if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) {
			if (explicitFactoryUsed) {
				log.warn(
						"Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
			}
			definition.getPropertyValues().add("sqlSessionTemplate",
					new RuntimeBeanReference(this.sqlSessionTemplateBeanName));
			explicitFactoryUsed = true;
		} else if (this.sqlSessionTemplate != null) {
			if (explicitFactoryUsed) {
				log.warn(
						"Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
			}
			definition.getPropertyValues().add("sqlSessionTemplate", this.sqlSessionTemplate);
			explicitFactoryUsed = true;
		}

		if (!explicitFactoryUsed) {
			log.debug("Enabling autowire by type for MapperFactoryBean with name '{}'.",  beanName);
			definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
		}
		
		definition.setLazyInit(false);
		
		if (checkCandidate(beanName, definition)) {
			BeanDefinitionHolder holder = new BeanDefinitionHolder(definition, beanName);
			registerBeanDefinition(holder, this.registry);
			
			return holder;
		}
		return null;
	}

	@Override
	public void setBeanClassLoader(ClassLoader classLoader) {
		loader = new MapperClassLoader(classLoader);
	}

}

class MapperClassLoader extends ClassLoader {
	private static final Map<String, Class> entityClassPool = new HashMap<>();
	private static final Class PARENT_INTERFACE = BaseMapper.class;
	private static final String PARENT_CLASSNAME = PARENT_INTERFACE.getName().replaceAll("\\.", "/");
	
	public MapperClassLoader(ClassLoader classLoader) {
		super(classLoader);
	}

	Class getMapperClass(String entityClassName) {
		
		synchronized (entityClassName.intern()) {
			if(! entityClassPool.containsKey(entityClassName)){
    			String entityClass = entityClassName.replaceAll("\\.", "/");
    			ClassWriter writer = new ClassWriter(0);
    			writer.visit(Opcodes.V1_8
    					, Opcodes.ACC_PUBLIC + Opcodes.ACC_ABSTRACT + Opcodes.ACC_INTERFACE
    					, entityClass + "Mapper"
    					, String.format("Ljava/lang/Object;L%s<L%s;>;", PARENT_CLASSNAME, entityClass)
    					, "java/lang/Object"
    					, new String[]{PARENT_CLASSNAME});
    			writer.visitEnd();
    	        byte[] data = writer.toByteArray();
    	        Class clazz = super.defineClass(entityClassName + "Mapper", data, 0, data.length);
    	        entityClassPool.put(entityClassName, clazz);
        		return clazz;
			} else {
				return entityClassPool.get(entityClassName);
			}
		}
	}
}
